package nr.co.mhgames.polyanim.loader;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import nr.co.mhgames.polyanim.Animation;
import nr.co.mhgames.polyanim.Group;
import nr.co.mhgames.polyanim.KeyFrame;
import nr.co.mhgames.polyanim.PolyAnim;
import nr.co.mhgames.polyanim.Polygon;
import nr.co.mhgames.polyanim.TimedKeyFrame;
import nr.co.mhgames.polyanim.Animation.AnimationStyle;
import nr.co.mhgames.polyanim.interpolation.Interpolator;
import nr.co.mhgames.polyanim.interpolation.SmoothStepInterpolator;
import nr.co.mhgames.polyanim.util.Vertex;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Animation loader that reads & writes Animations from/to XML files.
 * 
 * @author Mika Halttunen
 */
public class XMLAnimationLoader implements AnimationLoader {

	/**
	 * Read a PolyAnim animation from the given XML file and return the
	 * animation, or <code>null</code> if the file couldn't be loaded properly.
	 * 
	 * @param file
	 *            File name of the animation XML
	 * @return Animation instance
	 * @throws Exception
	 *             Thrown if anything fails
	 */
	public Animation readAnimation(File file) throws Exception {
		return readAnimation(new FileInputStream(file));
	}


	/**
	 * Read a PolyAnim animation from the given XML input stream and return the
	 * animation, or <code>null</code> if the animation couldn't be loaded
	 * properly.
	 * 
	 * @param input
	 *            Input stream of the animation XML
	 * @return Animation instance
	 * @throws Exception
	 *             Thrown if anything fails
	 */
	public Animation readAnimation(InputStream input) throws Exception {
		Animation anim = null;
		// Parse the XML file in
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		DocumentBuilder builder = factory.newDocumentBuilder();

		Document doc = builder.parse(input);

		// Check the root node
		Element rootElem = doc.getDocumentElement();
		String root = rootElem.getNodeName();
		if ("polyanim".equals(root) == false) {
			throw new Exception("The input is not a valid PolyAnim animation!");
		}

		// Version check
		String version = rootElem.getAttribute("version");
		if (!supportsVersion(version))
			throw new Exception("The version of the input (" + version
					+ ") is not supported in this PolyAnim version (" + PolyAnim.VERSION_STR + ")");

		// The animation node is assumed to be the first (only) child
		NodeList animations = rootElem.getElementsByTagName("animation");
		Element animElem = (Element) animations.item(0);

		// Start building the animation
		anim = new Animation(animElem.getAttribute("name"));

		// Playing style
		String style = animElem.getAttribute("style");
		if (style.equalsIgnoreCase("pingpong"))
			anim.setPlayingStyle(AnimationStyle.PINGPONG);

		// Interpolation style
		String interpolation = animElem.getAttribute("interpolation");
		if (interpolation.equalsIgnoreCase("smoothstep")) {
			// For backwards compatibility
			anim.setInterpolator(SmoothStepInterpolator.instance);
		} else if (interpolation.length() > 0) {
			// Create an instance of the interpolator and use that, unless the
			// class has a static instance member
			String params = animElem.getAttribute("interpolationParam");
			String packageName = Interpolator.class.getPackage().getName();
			String className = String.format("%s.%s%s", packageName, interpolation, interpolation
					.endsWith("Interpolator") ? "" : "Interpolator");

			Class<?> ipClass = Class.forName(className);
			try {
				// Use the static instance
				Interpolator ip = (Interpolator) ipClass.getField("instance").get(null);
				anim.setInterpolator(ip);
			} catch (Exception e) {
				// Create a new instance and parse it
				Interpolator ip = (Interpolator) ipClass.newInstance();
				ip.parseFrom(params);

				anim.setInterpolator(ip);
			}
		}

		// Looping
		Element loopingElem = (Element) animElem.getElementsByTagName("looping").item(0);
		for (Node child = loopingElem.getFirstChild(); child != null; child = child
				.getNextSibling()) {
			if (child.getNodeType() == Node.ELEMENT_NODE) {
				Element e = (Element) child;
				if (e.getNodeName().equals("start"))
					anim.setLoopStart(Float.parseFloat(e.getTextContent()));
				else if (e.getNodeName().equals("end"))
					anim.setLoopEnd(Float.parseFloat(e.getTextContent()));
				else if (e.getNodeName().equals("count"))
					anim.setLoopCount(Integer.parseInt(e.getTextContent()));
			}

		}

		// Parse the key frames
		Element framesElem = (Element) animElem.getElementsByTagName("keyframes").item(0);
		for (Node frame = framesElem.getFirstChild(); frame != null; frame = frame.getNextSibling()) {
			if (frame.getNodeType() == Node.ELEMENT_NODE && frame.getNodeName().equals("keyframe")) {
				Element frameElem = (Element) frame;
				TimedKeyFrame newFrame = parseKeyFrame(frameElem);
				anim.addKeyFrame(newFrame);
			}
		}

		// Check for errors
		anim.checkForFaults();

		return anim;
	}

	/** @see {@link #readAnimation(File)} */
	public Animation readAnimation(String file) throws Exception {
		return readAnimation(new File(file));
	}


	/**
	 * Parse a polygon (including its vertices) from the given XML DOM element.
	 * 
	 * @param polyElem
	 *            Polygon element
	 * @return Polygon instance
	 */
	private Polygon parsePolygon(Element polyElem) {
		Polygon poly = new Polygon();

		// Parse the color
		Element colorElem = (Element) polyElem.getElementsByTagName("color").item(0);
		int r = Integer.parseInt(colorElem.getAttribute("red"));
		int g = Integer.parseInt(colorElem.getAttribute("green"));
		int b = Integer.parseInt(colorElem.getAttribute("blue"));
		int a = Integer.parseInt(colorElem.getAttribute("alpha"));
		poly.setColor(r, g, b, a);

		// Parse the vertices
		Element vertsElem = (Element) polyElem.getElementsByTagName("vertices").item(0);
		for (Node v = vertsElem.getFirstChild(); v != null; v = v.getNextSibling()) {
			if (v.getNodeType() == Node.ELEMENT_NODE) {
				Element vertElem = (Element) v;
				float x = Float.parseFloat(vertElem.getAttribute("x"));
				float y = Float.parseFloat(vertElem.getAttribute("y"));

				poly.addVertex(x, y);
			}
		}

		return poly;
	}

	/**
	 * Parse a polygon group (including its contents) from the given XML DOM
	 * element.
	 * 
	 * @param groupElem
	 *            Group element
	 * @return Group instance
	 */
	private Group parseGroup(Element groupElem) {
		// Parse the name
		Group group = new Group();
		group.setName(groupElem.getAttribute("name"));

		// Get the properties
		Element props = (Element) groupElem.getElementsByTagName("properties").item(0);
		group.setDepthValue(Integer.parseInt(props.getAttribute("depth")));
		group.setVisible(Boolean.parseBoolean(props.getAttribute("visible")));

		// Parse transforms
		Element transElem = (Element) groupElem.getElementsByTagName("transform").item(0);
		for (Node child = transElem.getFirstChild(); child != null; child = child.getNextSibling()) {
			if (child.getNodeType() == Node.ELEMENT_NODE) {
				Element e = (Element) child;
				if (e.getNodeName().equals("offset")) {
					float x = Float.parseFloat(e.getAttribute("x"));
					float y = Float.parseFloat(e.getAttribute("y"));
					group.setOffset(x, y);
				} else if (e.getNodeName().equals("scale")) {
					float x = Float.parseFloat(e.getAttribute("x"));
					float y = Float.parseFloat(e.getAttribute("y"));
					group.setScale(x, y);
				} else if (e.getNodeName().equals("pivot")) {
					float x = Float.parseFloat(e.getAttribute("x"));
					float y = Float.parseFloat(e.getAttribute("y"));
					group.setPivot(x, y);
				} else if (e.getNodeName().equals("rotation")) {
					group.setRotation(Float.parseFloat(e.getTextContent()));
				}
			}
		}

		// Parse polygons
		Element polysElem = (Element) groupElem.getElementsByTagName("polygons").item(0);
		for (Node poly = polysElem.getFirstChild(); poly != null; poly = poly.getNextSibling()) {
			if (poly.getNodeType() == Node.ELEMENT_NODE) {
				Element polyElem = (Element) poly;
				Polygon newPoly = parsePolygon(polyElem);

				group.addPolygon(newPoly);
			}
		}


		return group;
	}

	/**
	 * Parse a key frame (including its contents) from the given XML DOM
	 * element.
	 * 
	 * @param frameElem
	 *            Key frame element
	 * @return TimedKeyFrame instance, that is KeyFrame with its duration
	 */
	private TimedKeyFrame parseKeyFrame(Element frameElem) {
		// Parse the duration
		float duration = Float.parseFloat(frameElem.getAttribute("duration"));
		TimedKeyFrame frame = new TimedKeyFrame();
		frame.setDuration(duration);

		KeyFrame keyFrame = new KeyFrame();

		// Parse the groups
		Element groupsElem = (Element) frameElem.getElementsByTagName("groups").item(0);
		for (Node group = groupsElem.getFirstChild(); group != null; group = group.getNextSibling()) {
			if (group.getNodeType() == Node.ELEMENT_NODE && group.getNodeName().equals("group")) {
				Element groupElem = (Element) group;
				Group newGroup = parseGroup(groupElem);

				keyFrame.addGroup(newGroup);
			}
		}

		frame.setFrame(keyFrame);
		return frame;
	}


	/**
	 * Write the given PolyAnim animation to the specified XML file.
	 * 
	 * @param file
	 *            File name of the animation XML
	 * @param animation
	 *            Animation instance to write
	 * @throws Exception
	 *             Thrown if anything fails
	 */
	public void writeAnimation(Animation animation, File file) throws Exception {
		writeAnimation(animation, new FileOutputStream(file));
	}

	/**
	 * Write the given PolyAnim animation as XML to the specified output stream.
	 * 
	 * @param animation
	 *            Animation instance to write
	 * @param output
	 *            Output stream for the XML
	 * @throws Exception
	 *             Thrown if anything fails
	 */
	public void writeAnimation(Animation animation, OutputStream output) throws Exception {
		// Construct the XML DOM document
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
		DocumentBuilder builder = factory.newDocumentBuilder();

		Document doc = builder.newDocument();

		doc.appendChild(doc.createComment("PolyAnim Animation"));

		// Create the root element (polyanim)
		Element root = doc.createElement("polyanim");
		root.setAttribute("version", PolyAnim.VERSION_STR);
		doc.appendChild(root);

		// Animation
		Element animElem = doc.createElement("animation");
		animElem.setAttribute("name", animation.getName());
		if (animation.getPlayingStyle() == AnimationStyle.PINGPONG)
			animElem.setAttribute("style", "pingpong");
		else
			animElem.setAttribute("style", "normal");
		root.appendChild(animElem);

		// Looping
		Element looping = doc.createElement("looping");
		Element loopStart = doc.createElement("start");
		loopStart.setTextContent(Float.toString(animation.getLoopStart()));
		looping.appendChild(loopStart);

		Element loopEnd = doc.createElement("end");
		loopEnd.setTextContent(Float.toString(animation.getLoopEnd()));
		looping.appendChild(loopEnd);

		Element loopCount = doc.createElement("count");
		loopCount.setTextContent(Integer.toString(animation.getLoopCount()));
		looping.appendChild(loopCount);
		animElem.appendChild(looping);

		// Append the key frames
		Element frames = doc.createElement("keyframes");
		int i = 1;
		for (TimedKeyFrame frame: animation.getKeyFrames()) {
			frames.appendChild(doc.createComment(String.format("Frame #%d", i++)));
			writeKeyFrame(frame, frames, doc);
		}
		animElem.appendChild(frames);


		// Write the XML file with formatting
		TransformerFactory tranFactory = TransformerFactory.newInstance();
		Transformer transformer = tranFactory.newTransformer();

		Source src = new DOMSource(doc);
		Result dst = new StreamResult(output);
		transformer.setOutputProperty(OutputKeys.INDENT, "yes");
		transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
		transformer.transform(src, dst);
	}

	/** @see {@link #writeAnimation(Animation, File)} */
	public void writeAnimation(Animation animation, String file) throws Exception {
		writeAnimation(animation, new File(file));
	}

	/**
	 * Write the given polygon to the XML DOM.
	 * 
	 * @param poly
	 *            Polygon
	 * @param root
	 *            Root element
	 * @param doc
	 *            DOM document
	 */
	private void writePolygon(Polygon poly, Element root, Document doc) {
		// Create the polygon element
		Element polyElem = doc.createElement("polygon");

		// Color
		Element color = doc.createElement("color");
		color.setAttribute("red", Integer.toString(poly.getColor().red));
		color.setAttribute("green", Integer.toString(poly.getColor().green));
		color.setAttribute("blue", Integer.toString(poly.getColor().blue));
		color.setAttribute("alpha", Integer.toString(poly.getColor().alpha));
		polyElem.appendChild(color);


		// Append the vertices
		Element vertices = doc.createElement("vertices");
		for (Vertex v: poly.getVertices()) {
			Element vertexElem = doc.createElement("vertex");
			vertexElem.setAttribute("x", Float.toString(v.position.x));
			vertexElem.setAttribute("y", Float.toString(v.position.y));
			vertices.appendChild(vertexElem);
		}
		polyElem.appendChild(vertices);
		root.appendChild(polyElem);
	}

	/**
	 * Write the given group to the XML DOM.
	 * 
	 * @param name
	 *            Group name
	 * @param group
	 *            Group
	 * @param root
	 *            Root element
	 * @param doc
	 *            DOM document
	 */
	private void writeGroup(String name, Group group, Element root, Document doc) {
		assert (name.equals(group.getName()));

		// Create the group element
		Element groupElem = doc.createElement("group");
		groupElem.setAttribute("name", name);
		root.appendChild(groupElem);

		// Create the group properties element
		Element props = doc.createElement("properties");
		props.setAttribute("depth", Integer.toString(group.getDepthValue()));
		props.setAttribute("visible", Boolean.toString(group.isVisible()));
		groupElem.appendChild(props);

		// Create the transform element
		Element trans = doc.createElement("transform");

		// Offset
		Element offset = doc.createElement("offset");
		offset.setAttribute("x", Float.toString(group.getOffset().x));
		offset.setAttribute("y", Float.toString(group.getOffset().y));
		trans.appendChild(offset);

		// Scale
		Element scale = doc.createElement("scale");
		scale.setAttribute("x", Float.toString(group.getScale().x));
		scale.setAttribute("y", Float.toString(group.getScale().y));
		trans.appendChild(scale);

		// Pivot
		Element pivot = doc.createElement("pivot");
		pivot.setAttribute("x", Float.toString(group.getPivot().x));
		pivot.setAttribute("y", Float.toString(group.getPivot().y));
		trans.appendChild(pivot);

		// Rotation
		Element rotation = doc.createElement("rotation");
		rotation.setTextContent(Float.toString(group.getRotation()));
		trans.appendChild(rotation);

		groupElem.appendChild(trans);

		// Append the polygons
		Element polyElem = doc.createElement("polygons");
		for (Polygon poly: group.getPolygons()) {
			writePolygon(poly, polyElem, doc);
		}
		groupElem.appendChild(polyElem);
	}

	/**
	 * Write the given key frame to the XML DOM.
	 * 
	 * @param frame
	 *            Key frame
	 * @param root
	 *            Root element
	 * @param doc
	 *            DOM document
	 */
	private void writeKeyFrame(TimedKeyFrame frame, Element root, Document doc) {
		// Create the key frame element
		Element frameElem = doc.createElement("keyframe");
		frameElem.setAttribute("duration", Float.toString(frame.getDuration()));
		root.appendChild(frameElem);

		// Append the polygon groups
		Element groups = doc.createElement("groups");
		int i = 1;
		for (String name: frame.getFrame().getGroups().keySet()) {
			Group group = frame.getFrame().getGroup(name);
			if (group == null)
				continue;

			groups.appendChild(doc.createComment(String.format("Group #%d", i++)));
			writeGroup(name, group, groups, doc);
		}
		frameElem.appendChild(groups);
	}


	public boolean supportsVersion(String version) {
		// Parse the version
		try {
			float ver = Float.parseFloat(version);
			if (ver >= 0.1f && ver <= PolyAnim.VERSION)
				return true;
		} catch (NumberFormatException e) {
		}

		return false;
	}
}
